![]() |
![]() |
|
Ihre Stärke spielen Schnittstellen aus, wenn Sie von mehreren Klassen implementiert werden. Jede Klasse weist dann dieselben Merkmale und Verhaltensweisen auf. Wenn Sie mit einer Klasse gearbeitet haben, die eine oder mehrere gängige Schnittstellen unterstützt, sollten Sie auch mit allen anderen Klassen umgehen können, welche die gleichen Schnittstellen unterstützen. Das trifft insbesondere auf die Schnittstelle IList zu, weil sie von sehr vielen Klassen des .NET Frameworks implementiert wird. Es ist daher empfehlenswert, sich insbesondere mit den Eigenschaften und Methoden dieser Schnittstelle vertraut zu machen. Beispiele dazu werden Ihnen im weiteren Verlauf dieses Buchs noch viele begegnen. 7.2.3 Die Klasse »ArrayList«
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Dim liste As New ArrayList |
| Dim arr() As Integer = {0, 10, 22, 9, 45} |
| liste.AddRange(arr) |
Liegt das Array bereits vor der Instanziierung von ArrayList vor, kann das Array auch direkt dem Konstruktor übergeben werden:
| Dim arr() As Integer = {0, 10, 22, 9, 45} |
| Dim liste As ArrayList = New ArrayList(arr) |
Die Add-Methode eines ArrayList-Objekts nimmt Elemente vom Typ Object entgegen. Somit dürfte klar sein, dass von einer ArrayList jeglicher Typus verwaltet werden kann und darüber hinaus auch nicht sichergestellt ist, dass alle Elemente typgleich sind. Das kann einerseits vorteilhaft sein, andererseits besteht aber auch manchmal das Bedürfnis, nur ganz bestimmte Objekte zu speichern. Auf diese Problematik werden wir weiter unten noch genau eingehen und auch Lösungen dazu erarbeiten.
Nehmen wir an, mehrere Objekte vom Typ ClassA sollen von einer ArrayList verwaltet werden. ClassA sei wie folgt definiert:
| Public Class ClassA |
| Public Prop As Integer |
| Public Sub New(ByVal x As Integer) |
| Prop = x |
| End Sub |
| End Class |
Im folgenden Code werden die beiden Referenzen obj1 und obj2 vom Typ ClassA der Auflistung col hinzugefügt. Die Auflistung col verwaltet danach obj1 über den Index 0 und obj2 über den Index 1.
| Dim obj1 As ClassA = New ClassA(1) |
| Dim obj2 As ClassA = New ClassA(2) |
| ' Klasse ArrayList instanziieren |
| Dim col As New ArrayList |
| ' obj1 und obj2 der Auflistung hinzufügen |
| col.Add(obj1) |
| col.Add(obj2) |
Damit hätten wir aktuell die folgenden Zuordnungen:
| Der Listeneintrag col(0) enthält die Referenz obj1. |
| Der Listeneintrag col(1) enthält die Referenz obj2. |
Mit Add haben Sie keinen Einfluss auf die Positionierung der Objekte innerhalb der Liste. Wollen Sie ein Objekt jedoch an einer bestimmten Position einsortieren, sollten Sie anstelle der Add-Methode die Methode Insert benutzen:
| Dim obj3 As ClassA = New ClassA(3) |
| col.Insert(1, obj3) |
Das über obj3 referenzierte Objekt wird damit unter dem Index 1 registriert. Falls man versucht, einen Index anzugeben, der größer ist als die Anzahl der Elemente in der Auflistung, kommt es zur Ausnahme ArgumentOutOfRangeException, da der letzte verfügbare Index immer genauso groß ist, wie die Auflistung Elemente enthält.
Was passiert aber mit dem Element obj2, das vorher die Position des Index 1 einnahm? Wir können das prüfen, indem wir eine Methode schreiben, der wir die Referenz auf die Auflistung als Argument übergeben. In der Methode werden die Elemente der übergebenen Auflistung vom ersten bis zum letzten Objekt an der Konsole ausgegeben:
| ' Auflistung enthält nur typgleiche Einträge |
| Public Sub GetListElements(ByVal list As IList) |
| Dim temp As ClassA |
| For Each temp In list |
| Console.Write("Collection-Index = {0}", list.IndexOf(temp)) |
| Console.WriteLine(" / Objekt-Nr.{0}", temp.Prop) |
| Next |
| End Sub |
Der Typ des Parameters ist IList. Wir hätten auch den Typ ArrayList wählen können, halten uns aber mit unserer Festlegung allgemeiner und haben damit eine Methode, die jeden beliebigen Auflistungstyp entgegennimmt, der IList implementiert und ClassA-Objekte verwaltet.
In der Methode GetListElements wird in einer For Each-Schleife die Auflistung vom ersten bis zum letzten Element durchlaufen. Die Laufvariable temp ist vom Typ ClassA deklariert, da wir wissen, dass unsere Auflistung nur Objekte dieses Typs enthält.
Typgleiche Objekte in einer der Auflistungsklassen zu verwalten, ist die Regel. Der Grund dafür ist einleuchtend, denn innerhalb der Schleife wird häufig ein typspezifisches Member des sich aktuell im Zugriff befindlichen Objekts aufgerufen. Verwaltet eine Auflistung unterschiedliche Typen, muss die Laufvariable der Schleife allgemeiner typisiert werden. Innerhalb des Schleifenblocks sind dann eine Typüberprüfung sowie eine Typumwandlung erforderlich, um auf ein spezifisches Merkmal des Objekts zuzugreifen. Das geht natürlich zu Lasten der Performance.
| ' Auflistung enthält unterschiedliche Typen |
| Public Sub GetListElements(ByVal list As IList) |
| Dim temp As Object |
| For Each temp In list |
| If TypeOf temp Is ClassA Then |
| Console.Write("Collection-Index = {0}", _ |
| list.IndexOf(temp)) |
| Console.WriteLine(" / Objekt-Nr.{0}", temp.Prop |
| End If |
| Next |
| End Sub |
Sehen wir uns nun die Ausgabe an, die von der Prozedur GetListElements in das Konsolenfenster geschrieben wird, nachdem wir mit Insert ein drittes Objekt an die zweite Listenposition gesetzt haben:
| Collection-Index = 0 / Objekt-Nr.1 |
| Collection-Index = 1 / Objekt-Nr.3 |
| Collection-Index = 2 / Objekt-Nr.2 |
Das ursprünglich dem Index 1 zugeordnete Objekt musste seine Position räumen – es verschiebt sich in der Liste um eine Position in Richtung Listenende, während sich obj3 wunschgemäß einreiht. Hätten wir noch weitere Elemente in unserer Auflistung, würde sich die Indizierung aller Folgeelemente ebenfalls um eine Position verschieben.
Ein ähnliches Verhalten zeigt sich auch, wenn wir mit Remove oder RemoveAt aus der Auflistung einen Eintrag löschen: Der freigegebene Index bleibt nicht unbelegt, sondern bewirkt eine Indexverschiebung aller Nachfolgeelemente. Löschen wir beispielsweise das Element mit dem Index 2, rutscht das Objekt mit dem ursprünglichen Index 3 an die frei gewordene Position 2, das Objekt mit dem Index 4 füllt die Lücke des Index 3 usw. (siehe auch Abbildung 7.2).

Hier klicken, um das Bild zu Vergrößern
Abbildung 7.2 Listenverwaltung beim Löschen
Die Tragweite dieser Elementverwaltung ist weitreichend, denn es kann zu keinem Zeitpunkt die eindeutige Zuordnung eines Objekts zu einem bestimmten Index in der Auflistung garantiert werden.
Auf der Buch-CD finden Sie den Programmcode des Beispiels unter
Auflistungen zeichnen sich durch die beiden Interfaces IEnumerable und ICollection aus. Aus der letztgenannten stammt die Methode CopyTo, die es ermöglicht, die Einträge einer Auflistung in ein Array zu kopieren.
| Dim col As New ArrayList |
| col.Add("Anton") |
| col.Add("Gustaf") |
| col.Add("Fritz") |
| Dim strArr(10) As String |
| col.CopyTo(strArr, 3) |
Der zweite Parameter von CopyTo gibt den Startindex im Array an, ab dem kopiert wird. Das Array muss groß genug sein, um alle Elemente aufzunehmen, sonst wird ein Fehler ausgelöst. Handelt es sich bei den zu kopierenden Einträgen um Objektreferenzen, werden nicht die Objekte, sondern nur die Referenzen kopiert. ArrayList überlädt CopyTo, so dass auch spezifizierte Teilbereiche der Liste kopiert werden können.
Die von ArrayList verwalteten Objekte sind sortierbar. Das ist keineswegs eine Selbstverständlichkeit, sondern ein wichtiges Charakteristikum dieser Klasse, wie wir später noch im Vergleich mit anderen Auflistungen feststellen werden.
Um die Mitglieder zu sortieren, wird die Methode Sort auf der ArrayList-Referenz aufgerufen. Sort ist mehrfach überladen. Wir wollen uns zunächst mit der parameterlosen Version beschäftigen:
| Public Overridable Sub Sort() |
Die Regel, nach der im deutschsprachigen Raum sortiert wird, vergleicht die Zeichen unter Berücksichtigung der Groß- und Kleinschreibung wie folgt:
| 1 < 2 ... < a < A < b < B < c < C ... < y < Y < z < Z |
Um die verwalteten Objekte einer ArrayList mit der parameterlosen Sort-Methode zu sortieren, müssen die Objekte die Schnittstelle IComparable implementieren. Diese Schnittstelle enthält nur die Methode CompareTo. Eine Klasse, die IComparable implementiert, garantiert, die Methode CompareTo zu veröffentlichen. Darauf ist die Sort-Methode der ArrayList angewiesen. Was eine Schnittstellenmethode leisten muss, ist der jeweiligen Dokumentation zu entnehmen. Aus der .NET-Dokumentation zu CompareTo können wir entnehmen, dass das aktuelle Objekt mit dem des Parameters verglichen wird. Als Resultat liefert der Methodenaufruf einen der drei folgenden Werte:
| < 0, wenn das aktuelle Objekt »kleiner« als das Objekt obj ist |
| 0, wenn das aktuelle Objekt »gleich« dem Objekt obj ist |
| > 0, wenn das aktuelle Objekt »größer« als das Objekt obj ist |
Die Kriterien, was im Vergleich als »kleiner«, »gleich« und »größer« bewertet wird, muss die Klasse festlegen, welche die Schnittstelle IComparable implementiert. Sehen wir uns dazu ein Beispiel an.
| Public Class HoldValue |
| Implements IComparable |
| Public IntVar As Integer |
| Public Sub New(ByVal x As Integer) |
| IntVar = x |
| End Sub |
| Public Function CompareTo(ByVal obj As Object) _ |
| As Integer Implements IComparable.CompareTo |
| Dim val As HoldValue = obj |
| If val.IntVar < Me.IntVar Then |
| Return 1 |
| ElseIf (val.IntVar = Me.IntVar) Then |
| Return 0 |
| Else |
| Return –1 |
| End If |
| End Function |
| End Class |
Die Klasse HoldValue implementiert IComparable. Daher sind Objekte dieses Typs darauf vorbereitet, in einer ArrayList sortiert zu werden. Die Sortierreihenfolge soll sich am Inhalt des Felds intVar orientieren. In der Methode CompareTo wird die dem Parameter übergebene Referenz zuerst in den Typ HoldValue konvertiert und einer lokalen Variablen zugewiesen. Anschließend folgt ein Vergleich zwischen den Feldwerten des aktuellen und des übergebenen Objekts.
Natürlich wollen wir nun auch testen, ob wir unser Ziel erreicht haben. Dazu dient der folgende Testcode:
| Sub Main() |
| Dim arrList As New ArrayList |
| Dim obj1 As HoldValue = New HoldValue(17) |
| arrList.Add(obj1) |
| Dim obj2 As HoldValue = New HoldValue(110) |
| arrList.Add(obj2) |
| Dim obj3 As HoldValue = New HoldValue(5) |
| arrList.Add(obj3) |
| arrList.Sort() |
| Dim i As Integer = 0 |
| Dim temp As HoldValue |
| For Each temp In arrList |
| Console.WriteLine("Element{0} – Wert: {1}", _ |
| i, temp.IntVar) |
| i += 1 |
| Next |
| End Sub |
An der Konsole werden die Werte der Felder in der Reihenfolge 5, 17, 100 ausgegeben, obwohl die ursprüngliche Reihenfolge in der Liste eine andere war. Der Vergleich und die anschließende Sortierung findet also wie erwartet statt.
Der Code lässt sich aber auch noch eleganter formulieren. Wenn Sie sich in der .NET-Dokumentation die Definition der Struktur Int32 ansehen, werden Sie feststellen, dass dieser Typ seinerseits selbst die Schnittstelle IComparable implementiert. Es ist daher nahe liegend, den Vergleich am Feld intVar direkt vorzunehmen:
| Public Function CompareTo(ByVal obj As Object) _ |
| As Integer Implements Comparable.CompareTo |
| Dim val As HoldValue = obj |
| Return Me.IntVar.CompareTo(val.IntVar) |
| End Function |
Damit können wir uns aber noch nicht ganz zufrieden geben, denn alle denkbaren Szenarien werden von unserer Implementierung noch nicht berücksichtigt. Es könnte nämlich auch ein Objekt übergeben werden, das mit dem aktuellen nicht vergleichbar ist, beispielsweise:
| Dim kreis As Circle = New Circle(5) |
| Dim val As HoldValue = New HoldValue(8) |
| Dim x As Integer = val.CompareTo(kreis) |
Wenn Sie die Methode CompareTo implementieren, sollten Sie diesem Fall ebenso berücksichtigen wie die Eventualität, dass das übergebene Objekt noch nicht initialisiert und daher Nothing ist. Die Implementierung, die diese beide Szenarien einbezieht, sieht wie folgt aus:
| Public Function CompareTo(ByVal obj As Object) _ |
| As Integer Implements IComparable.CompareTo |
| ' Prüfen, ob der Parameter ein Nothing-Verweis ist |
| If obj Is Nothing Then |
| Return 1 |
| End If |
| ' Prüfen, ob beide Typen gleich sind |
| If (Not TypeOf obj Is HoldValue) Then |
| Throw New ArgumentException("Ungültiger Vergleich") |
| End If |
| ' Vergleich der beiden Objekte |
| Dim val As HoldValue = obj |
| Return Me.IntVar.CompareTo(val.IntVar) |
| End Function |
Generell sollten Sie die Methode CompareTo der Schnittstelle IComparable wie gezeigt implementieren, um gegen alle unzulässigen Aufrufe gewappnet zu sein. Es wird zuerst überprüft, ob dem Parameter Nothing übergeben wurde. Der Vergleich sollte daraufhin abgebrochen werden und als Resultat einen Wert größer 0 liefern. Damit wird ein Nothing-Verweis vor einem Objektverweis einsortiert. Unterscheiden sich die beiden Typen des anstehenden Vergleichs, wird die Ausnahme ArgumentException ausgelöst und muss vom Aufrufer behandelt werden.
Auf der Buch-CD finden Sie den Programmcode des Beispiels unter:
Das Sortieren einer ArrayList mit der parameterlosen Sort-Methode gestattet nur ein Vergleichskriterium. Manchmal ist es aber erforderlich, unterschiedliche Sortierkriterien zu berücksichtigen. Nehmen wir zum Beispiel die Klasse Person, welche die beiden Felder Name und Wohnort beschreibt.
| Class Person |
| Public Name As String |
| Public Wohnort As String |
| Public Sub New(ByVal name As String, ByVal ort As String) |
| name = name |
| Wohnort = ort |
| End Sub |
| End Class |
Würden die Klassen die Schnittstelle IComparable implementieren, müsste die Entscheidung getroffen werden, nach welchem Feld Objekte dieser Klasse sortiert werden können. Nun sollen beide Möglichkeiten angeboten werden.
Die Lösung des Problems führt über die Bereitstellung so genannter Vergleichsklassen, welche die Schnittstelle IComparer implementieren. In jeder Vergleichsklasse wird genau ein Vergleichskriterium festgelegt. Wollen wir einen bestimmten Objektvergleich erzwingen, müssen wir der Sort-Methode mitteilen, welche Vergleichsklasse dafür bestimmt ist. Dafür stehen uns zwei Überladungen zur Verfügung, denen die Referenz auf ein Objekt übergeben wird, das die Schnittstelle IComparer implementiert:
| Public Overridable Sub Sort(IComparer) |
| Public Overridable Sub Sort(Integer, Integer, IComparer) |
Mit der Überladung, die zwei Integer erwartet, können der Startindex und die Länge des zu sortierenden Bereichs bestimmt werden. Bei sehr großen Auflistungen steigert das die Performance, da Sortiervorgänge sehr rechenintensiv sind.
Die Schnittstelle IComparer stellt eine Methode für den Vergleich zweier Objekte bereit:
| Function Compare(x As Object, y As Object) As Integer |
Compare funktioniert ähnlich der weiter oben erörterten Methode CompareTo und gibt die folgenden Werte zurück:
| < 0, wenn das erste Objekt »kleiner« als das zweite Objekt ist |
| 0, wenn das erste Objekt »gleich« dem zweiten Objekt ist |
| > 0, wenn das erste Objekt »größer« als das zweite Objekt ist |
| Hinweis |
|
Der große Unterschied zwischen den beiden Schnittstellenmethoden IComparable.CompareTo und IComparer.Compare ist die Parameterliste und der daraus resultierende Methodenaufruf. CompareTo nimmt eine Referenz entgegen, die mit dem aktuellen Objekt verglichen wird, während Compare zwei zu vergleichende Referenzen übergeben werden. Somit ist diese Methode auch unabhängig von der Me-Referenz. Die Schnittstelle IComparer bietet sich daher auch an, wenn Sie die Objekte eines Typs vergleichen wollen, der nicht IComparable implementiert. |
Für die Klasse Person wollen wir nun die beiden Vergleichsklassen NameComparer und WohnortComparer entwickeln, die gemäß Forderung die Schnittstelle IComparer implementieren und nach Wohnort bzw. Name sortieren.
| ' Vergleichsklasse – Kriterium 'Wohnort' |
| Class WohnortComparer |
| Implements IComparer |
| Public Function Compare(ByVal x As Object, ByVal y As Object) _ |
| As Integer Implements IComparer.Compare |
| ' Prüfen auf Nothing-Übergabe |
| If (x Is Nothing And y Is Nothing) Then |
| Return 0 |
| End If |
| If (x Is Nothing) Then |
| Return 1 |
| End If |
| If (y Is Nothing) Then |
| Return –1 |
| End If |
| ' Typüberprüfung |
| If (Not x.GetType Is y.GetType) Then |
| Throw New ArgumentException("Ungültiger Vergleich") |
| End If |
| ' Vergleich |
| Return x.Wohnort.CompareTo(y.wohnort) |
| End Function |
| End Class |
| ' Vergleichsklasse – Kriterium 'Name' |
| Class NameComparer |
| Implements IComparer |
| Public Function Compare(ByVal x As Object, ByVal y As Object) _ |
| As Integer Implements IComparer.Compare |
| ' Prüfen auf Nothing-Übergabe |
| If (x Is Nothing And y Is Nothing) Then |
| Return 0 |
| End If |
| If (x Is Nothing) Then |
| Return 1 |
| End If |
| If (y Is Nothing) Then |
| Return –1 |
| End If |
| ' Typüberprüfung |
| If (Not x.GetType Is y.GetType) Then |
| Throw New ArgumentException("Ungültiger Vergleich") |
| End If |
| ' Vergleich |
| Return x.Zuname.CompareTo(y.Zuname) |
| End Function |
| End Class |
Die Implementierung ähnelt der der Methode CompareTo. Zuerst sollte wieder ein Vergleich mit Nothing durchgeführt werden und anschließend eine Prüfung, ob beide Parameter denselben Typ beschreiben oder zumindest einen vergleichbaren Typ besitzen. Sollte keine Bedingung zutreffen, kann der Vergleich der Objekte erfolgen. Dabei unterstützt uns die Klasse String, die ihrerseits die IComparable-Schnittstelle implementiert, mit der Methode CompareTo.
Haben wir ein ArrayList-Objekt mit Person-Objekten gefüllt, steht es uns frei, welche Vergleichsklasse wir zur Sortierung der Objekte benutzen, denn beide sind auf dieselbe Schnittstelle zurückzuführen und gegenseitig austauschbar.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 7\IComparerDemo |
| ' ---------------------------------------------------------- |
| Module Module1 |
| Sub Main() |
| Dim arrList As New ArrayList |
| ' ArrayList füllen |
| Dim pers1 As Person = New Person("Meier", "Berlin") |
| arrList.Add(pers1) |
| Dim pers2 As Person = New Person("Arnhold", "Köln") |
| arrList.Add(pers2) |
| Dim pers3 As Person = New Person("Graubär", "Aachen") |
| arrList.Add(pers3) |
| ' nach Wohnorten sortieren |
| arrList.Sort(New WohnortComparer()) |
| Console.WriteLine("Liste nach Wohnorten sortiert") |
| ShowSortedList(arrList) |
| ' nach Namen sortieren |
| arrList.Sort(New NameComparer()) |
| Console.WriteLine("Liste nach Namen sortiert") |
| ShowSortedList(arrList) |
| Console.ReadLine() |
| End Sub |
| Sub ShowSortedList(ByVal liste As IList) |
| Dim temp As Person |
| For Each temp In liste |
| Console.Write("Name = {0,-12}", temp.Zuname) |
| Console.WriteLine("Wohnort = {0}", temp.Wohnort) |
| Next |
| Console.WriteLine() |
| End Sub |
| End Module |
Mit der statischen Methode Adapter kann ein Wrapper (darunter ist eine Klasse zu verstehen, die eine andere einhüllt) um ein IList-Objekt gelegt werden. Der Rückgabewert ist die Referenz auf ein neues ArrayList-Objekt, auf dessen Methoden sich das IList-Objekt manipulieren lässt.
| Public Shared Function Adapter(IList)As ArrayList |
Wie Sie die Methode Adapter einsetzen können, möchte ich Ihnen an einem Beispiel zeigen. Wie Sie der .NET-Dokumentation zu IList entnehmen können, implementiert auch ein gewöhnliches Array diese Schnittstelle. Ein Array kann allerdings nicht sortiert werden. Über den Aufruf von Adapter wird das allerdings möglich.
Nehmen wir an, die Klasse Person sei wie folgt definiert:
| Public Class Person |
| Public Zuname As String |
| Public Alter As Integer |
| Public Sub New(ByVal name As String, ByVal alt As Integer) |
| Zuname = name |
| Alter = alt |
| End Sub |
| End Class |
Ein Array vom Typ Person soll mehrere Objekte enthalten, die dem Namen nach sortiert werden sollen. Dazu rufen wir die statische Methode Adapter unter Übergabe des Arrays auf und weisen die zurückgelieferte Referenz der Methode einer ArrayList-Variablen zu.
| Dim pers(2) As Person |
| pers(0) = New Person("Peter", 15) |
| pers(1) = New Person("Alfred", 33) |
| pers(2) = New Person("Hugo", 26) |
| Dim liste As ArrayList = ArrayList.Adapter(pers) |
Die Elemente des Arrays sollen jetzt dem Namen nach sortiert werden. Dazu bietet sich eine Überladung der Methode Sort der ArrayList an:
| Public Overridable Sub Sort(IComparer) |
Da ein Array die Schnittstelle IComparer nicht implementiert, die notwendig wäre, um diese Überladung aufzurufen, müssen wir eine Vergleichsklasse, welche die Schnittstelle IComparer implementiert, bereitstellen:
| Public Class SortByName |
| Implements IComparer |
| Public Function Compare(ByVal x As Object, _ |
| ByVal y As Object) As Integer Implements IComparer.Compare |
| Return x.Zuname.CompareTo(y.Zuname) |
| End Function |
| End Class |
Mit dem Aufruf von Sort unter Übergabe eines Objekts vom Typ der Vergleichsklasse SortByName können wir über den bereitgestellten Wrapper die Elemente des Arrays pers in die gewünschte Reihenfolge bringen:
| liste.Sort(new SortByName()) |
Den vollständigen Programmcode zu diesem Beispiel finden Sie auf der Buch-CD unter:
.\Kapitel 7\ArrayList_Adapter)
IList-Auflistungen verwalten die Objekte über Indizes. Dieses Konzept hat aber einen Nachteil: Wenn man nach einem bestimmten Element sucht und dessen Position nicht kennt, muss man die Liste so lange durchlaufen, bis man eine Übereinstimmung findet. Enthält die Auflistung sehr viele Einträge, kann das sehr zeitaufwändig sein und kostet Rechenleistung.
Kommt es nicht auf die Reihenfolge der Elemente an, kann man sich für eine Auflistung, die das Interface IDictionary implementiert, entscheiden. Dazu gehört die Klasse Hashtable, die unten vorgestellt wird. In diesen Auflistungen kann ein bestimmtes Element zwar schneller gefunden werden, allerdings muss man dabei in Kauf nehmen, keinen Einfluss auf die Positionierung der Elemente in der Liste zu haben. IDictionary-Collections organisieren die Elemente in einer für sie passenden Reihenfolge.
Um nach einem Element in einer IDictionary-Auflistung zu suchen, wird eine Schlüsselinformation benötigt, der ein Wert zugeordnet ist. IDictionary-Auflistungen enthalten Elemente mit Schlüssel-Wert-Kombinationen. Der Schlüssel muss eindeutig sein und darf nicht den Inhalt Nothing haben. In einer IList-Collection entspricht der Schlüssel dem Index. Der wesentliche Unterschied ist dabei jedoch, dass der Schlüssel nicht garantiert, eindeutig einem bestimmten Eintrag zugeordnet zu sein.
Stellen Sie sich dazu vor Sie beabsichtigten, die Mitarbeiter eines Unternehmens in einer Auflistung zu verwalten. Jeder Mitarbeiter ist über eine eindeutige Personalnummer identifizierbar. Diese Personalnummer beschreibt gleichzeitig, welche persönlichen Daten zu dem Mitarbeiter gehören:
| 0999–123–3 = Franz Fischer |
| 0100–288–3 = Peter Müller |
| 6771–771–1 = Marita Kohl |
Hinter jeder Personalnummer verbirgt sich genau ein Mitarbeiter, aber einem Mitarbeiter könnten durchaus auch zwei Personalnummern zugewiesen werden – vielleicht weil er zwei separat honorierte Positionen besetzt. Diese drei Wertpaare ließen sich problemlos durch eine IDictionary-Auflistung abbilden. Der Schlüssel würde durch die Personalnummer beschrieben, der Name entspräche dem Wert. In der Realität würde man dann allerdings wahrscheinlich sinnvollerweise den Namen durch eine Objektreferenz ersetzen, die auf das dem Mitarbeiter zugeordnete Objekt verweist. Der Schlüssel kann durchaus selbst ein Objekt sein, wird aber häufig durch Zeichenfolgen beschrieben.
Die meisten der von IDictionary veröffentlichten Methoden sind uns bereits aus der Schnittstelle IList bekannt. Das erleichtert zwar einerseits die Einarbeitung, zwingt uns aber andererseits dennoch in einigen Fällen zu einer etwas genaueren Betrachtung.
Jeder Listeneintrag in einer IDictionary-Auflistung wird durch ein Schlüssel-Wert-Paar beschrieben, was sich in der Parameterliste der Add-Methode niederschlägt:
| Sub Add(key As Object, value As Object) |
Der erste Parameter wird als Schlüssel für das hinzuzufügende Element verwendet und sorgt für die Identifizierbarkeit innerhalb einer Liste, der zweite ist die Referenz auf das hinzuzufügende Element. Wir stoßen hier zum ersten Mal auf die Tatsache, dass von IDictionary-Auflistungen anstelle eines Indizes ein Schlüssel verwendet wird.
Der Schlüssel begleitet uns durch alle Methoden und wird auch von Remove zum Entfernen eines Objekts aus der Auflistung verwendet:
| Sub Remove(key As Object) |
Da IDictionary-Objekte nicht über Indizes verwaltet werden, brauchen nach dem Löschen eines Elements etwaige Folgeelemente auch keine Lücke zu schließen.
Gibt man einen Schlüssel an, der sich noch nicht in der Auflistung befindet, wird das Element hinzugefügt. Dabei bleibt der Wert leer, ist also Nothing, was durchaus zulässig ist.
Die Schlüssel und die Werte werden in eigenen Auflistungen verwaltet. Die Referenz auf diese internen Auflistungen liefern die Eigenschaften Keys und Values.
| ReadOnly Property Keys As ICollection |
| ReadOnly Property Values As ICollection |
Mit Clear kann eine IDictionary-Auflistung geleert werden, mit Contains können wir prüfen, ob ein bestimmter Schlüssel bereits in der Liste enthalten ist.
| Eigenschaft/Methode | Beschreibung |
| Add | Hinzufügen eines Objekts zur Auflistung. |
| Remove | Löschen eines Elements aus der Auflistung. |
| Item | Zugriff auf ein Element der Auflistung. |
| Keys | Liefert alle in der Liste verwendeten Schlüssel zurück. |
| Values | Liefert alle in der Liste verwendeten Werte zurück. |
| Clear | Löscht alle Elemente der Auflistung. |
| Contains | Prüft, ob ein bestimmter Schlüssel in der Auflistung enthalten ist. |
Die wichtigste Auflistung, die das IDictionary-Interface implementiert, wird von der Klasse Hashtable beschrieben. Dieser Auflistungstyp ist eine Datenstruktur, die ein schnelles Suchen nach Objekten erlaubt. Der Name rührt daher, dass für die Verwaltung der Elemente ein Hash-Wert für den Schlüssel verwendet wird. Zum Erzeugen des Hash-Werts wird intern die von Object geerbte Methode GetHashCode ausgeführt.
Im folgenden Beispiel wird eine Hashtabelle erzeugt, die vier Objekte vom Typ ClassA sowie eine Zeichenfolge verwaltet. Damit wir sehen, wie wir über den Tabelleneintrag auf das Member eines registrierten Elements zugreifen, ist in der Definition der ClassA die öffentliche Eigenschaft IntVar deklariert, der wir über den Konstruktor einen Wert übergeben. Im Beispielcode werden die wichtigsten Methoden einer Hashtabelle benutzt, um Informationen sowohl über die Elemente als auch über die Listeneinträge zu erhalten.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 7\Hashtabelle |
| ' ---------------------------------------------------------- |
| Module Module1 |
| Dim obj1 As ClassA = New ClassA(1) |
| Dim obj2 As ClassA = New ClassA(2) |
| Dim obj3 As ClassA = New ClassA(3) |
| Dim obj4 As ClassA = New ClassA(4) |
| Dim hash As Hashtable |
| Sub Main() |
| hash = New Hashtable() |
| ' Objekte der Hashtabelle hinzufügen |
| AddObjects() |
| ' Liste der Schlüssel ausgeben |
| GetKeyList() |
| ' Liste der Werte ausgeben |
| GetValueList() |
| ' Liste der Schlüssel und Werte ausgeben |
| GetCompleteList() |
| Console.WriteLine() |
| ' Zugriff auf ein bestimmtes Element |
| Console.Write("Geben Sie nun den Schlüssel ") |
| Console.Write("des Objekts ein, dessen Eigenschaft ") |
| Console.Write("intVar Sie auswerten wollen: ") |
| Dim input As String = Console.ReadLine() |
| ' prüfen, ob der Schlüssel sich in der Hashtabelle befindet |
| If (hash.Contains(input)) Then |
| Console.Write("Das Objekt {0} ", input) |
| Console.Write("hat in intVar den Inhalt {0}", _ |
| hash(input).IntVar) |
| Console.WriteLine() |
| Else |
| Console.WriteLine("Nicht Element der Hashtabelle") |
| End If |
| ' anhand des Wertes prüfen, ob sich ein Objekt |
| ' bereits in der Hashtabelle befindet |
| Console.Write("Aufruf von ContainsValue: ") |
| If (hash.ContainsValue(obj2)) Then |
| Console.WriteLine("Das Objekt ist enthalten.") |
| Else |
| Console.WriteLine("Das Objekt ist nicht enthalten.") |
| End If |
| Console.ReadLine() |
| End Sub |
| ' Ausgabe der Wertliste |
| Public Sub GetValueList() |
| Console.WriteLine() |
| Console.WriteLine("===== Werteliste =====") |
| Dim obj As Object |
| For Each obj In hash.Values |
| Console.WriteLine(obj) |
| Next |
| End Sub |
| ' Ausgabe der Schlüsselliste |
| Public Sub GetKeyList() |
| Console.WriteLine() |
| Console.WriteLine("===== Schlüsselliste =====") |
| Dim obj As Object |
| For Each obj In hash.Keys |
| Console.WriteLine(obj) |
| Next |
| End Sub |
| ' Schlüssel-Wert-Paar über ein DictionaryEntry-Objekt ausgeben |
| Public Sub GetCompleteList() |
| Console.WriteLine() |
| Console.WriteLine("===== Schlüssel-/Wertepaare =====") |
| Dim dicEntry As DictionaryEntry |
| For Each dicEntry In hash |
| Console.Write(dicEntry.Key) |
| Console.WriteLine(" – {0}", dicEntry.Value) |
| Next |
| End Sub |
| ' Objekte der Hashtabelle hinzufügen |
| Public Sub AddObjects() |
| hash.Add("eins", obj1) |
| hash.Add("zwei", obj2) |
| hash.Add("drei", obj3) |
| hash.Add("vier", obj4) |
| hash.Add("fünf", "Hallo") |
| End Sub |
| End Module |
| Class ClassA |
| Public IntVar As Integer |
| Public Sub New(ByVal x As Integer) |
| IntVar = x |
| End Sub |
| End Class |
Das Objekt hash vom Typ Hashtable wird mit dem parameterlosen Konstruktor erzeugt, der meistens ausreichen dürfte. Anschließend werden in der benutzerdefinierten Methode AddObjects vier ClassA-Objekte in der Hashtabelle registriert. Darüber hinaus wird auch noch eine Zeichenfolge als fünftes Objekt eingetragen. Dem Aufruf der Methode Add werden dazu Schlüssel und Wert übergeben. Im Beispiel ist der Schlüssel eine Zeichenfolge, die Objektreferenz stellt den Wert dar. Ist ein Schlüssel bereits in der Hashtabelle enthalten, kommt es zur Auslösung der Ausnahme ArgumentException.
Der Inhalt einer Hashtabelle lässt sich abfragen – sowohl die Liste der Schlüssel als auch die Liste der Werte. Dazu dienen die Eigenschaften Keys und Values. In den Methoden GetKeyList und GetValueList wird in jeweils einer For Each-Schleife die Werte- bzw. Schlüsselliste durchlaufen. Beachten Sie, dass die Laufvariablen der Schleifen nicht dazu benutzt werden können, auf das Listenelement zuzugreifen, um in unserem Fall beispielsweise das Feld IntVar auszuwerten.
Um auf ein Listenelement in einer For Each-Schleife zugreifen zu können, müssen Sie die Laufvariable vom Typ DictionaryEntry deklarieren. Tatsächlich sind die Elemente in einer HashTable von diesem Typ. DictionaryEntry ist eine Struktur, die das Schlüssel-Wert-Paar für einen Hashtabelleneintrag enthält. Über die Eigenschaften Key und Value können wir die notwendigen Informationen beziehen. Während uns Key nur den Schlüssel liefert, können wir über den Rückgabewert von Value auf das Objekt zugreifen:
| Dim dicEntry As DictionaryEntry |
| For Each dicEntry In hash |
| Console.Write(dicEntry.Key) |
| Console.WriteLine(" – {0}", dicEntry.Value) |
| Next |
| Hinweis |
|
Dass die Einträge in einer Hashtabelle vom Typ DictionaryEntry sind, müssen Sie berücksichtigen, wenn Sie mit der Methode CopyTo die Einträge in ein Array kopieren wollen. Das Array muss dann vom diesem Typ oder vom Typ Object sein. |
Eine HashTable dient zur Verwaltung mehrerer meist gleichartiger Objekte und hat im Vergleich zu anderen Auflistungen den Vorteil, einen sehr schnellen Zugriff über den Indexer zu ermöglichen. Im Beispiel oben wird der Benutzer an der Konsole dazu aufgefordert, einen Schlüssel anzugeben, nach dem in der Hashtabelle gesucht werden soll. Ob der Schlüssel einem Element der Auflistung zugeordnet werden kann, wird durch die Methode Contains festgestellt, die einen booleschen Wert zurückliefert:
| If hash.ContainsValue(input) Then |
Analog könnte man auch die Methode ContainsKey benutzen, die sich in keiner Weise von Contains unterscheidet.
Nicht nur über den Schlüssel lässt sich prüfen, ob ein Element Mitglied der Hashtabelle ist. Auch über den booleschen Rückgabewert von ContainsValue ist das möglich. Im Beispiel wird dazu direkt die Referenz obj2 übergeben, die natürlich immer zu derselben Konsolenausgabe führt:
| If hash.ContainsValue(obj2) Then ... |
Weder die Klasse Queue noch die Klasse Stack implementiert das Interface IList oder IDictionary. Dennoch werden beide den Auflistungen zugerechnet, weil sie die Schnittstellen ICollection und somit auch IEnumerable implementieren.
Stack ist eine Datenstruktur, die nach dem LIFO-Prinzip (Last-in-First out) arbeitet: Das Element, das als letztes eingefügt wurde, wird beim folgenden Lesevorgang wieder entnommen. Daraus folgt, dass man auf das Element, das als erstes auf den Stack gelegt worden ist, erst dann wieder zugreifen kann, wenn alle anderen Elemente den Stack verlassen haben.
Ein Queue-Objekt ist das Pendant zu Stack. Es arbeitet nach dem FIFO-Prinzip (First-in-First out), das besagt, dass das zuerst in die Queue geschobene Element auch als erstes wieder entnommen wird. Das Prinzip gleicht also einer Warteschlange an der Kasse eines Fußballstadions.
Schauen wir uns in einem Beispiel an, wie man mit der Klasse Stack arbeitet.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 7\StackClass |
| ' ---------------------------------------------------------- |
| Module Module1 |
| Sub Main() |
| Dim myStack As Stack = New Stack(10) |
| ' Stack füllen |
| For i As Integer= 0 To 10 |
| myStack.Push(i * i) |
| Next |
| ' Ausgabe an der Konsole |
| PrintStack(myStack) |
| Console.ReadLine() |
| End Sub |
| Public Sub PrintStack(ByVal obj As Stack) |
| ' alle Elemente aus dem Stack holen |
| Do While (obj.Count <> 0) |
| Console.WriteLine(obj.Pop()) |
| Loop |
| End Sub |
| End Module |
Das Hinzufügen neuer Elemente geschieht durch den Aufruf der Methode Push, die als Argument ein Objekt erwartet. Im Beispielcode wird eine Schleife durchlaufen, in der insgesamt elf Zahlen auf den Stack gelegt werden. Es handelt sich dabei immer um das Quadrat des aktuellen Schleifenzählers.
Zugegriffen werden kann nur auf das oberste Element im Stack. Dabei handelt es sich immer um das Objekt, das als letztes mit der Push-Methode auf den Stack gelegt wurde.
Es bieten sich zwei Alternativen an, das oberste Element auszuwerten: Mit Pop wird das oberste Element nicht nur zurückgeliefert, sondern gleichzeitig auch der Stack-Verwaltung entzogen. Mit Peek erhält man zwar die Referenz, ohne es jedoch gleichzeitig zu entfernen. Im Beispiel wird der Stack so lange mit Pop abgegriffen, bis die Liste wieder leer ist. Die Reihenfolge der Zahlen beim Hinzufügen lautete:
| 0 1 4 9 16 25 36 ... 81 100 |
| 100 81 64 ... 25 16 9 4 1 0 |
Der Aufruf des parameterlosen Konstruktors der Klasse Stack führt zu einer Standardkapazität von 32 Elementen, die bei Bedarf automatisch erhöht wird, um weitere Elemente aufzunehmen. Dabei werden alle Elemente in ein neues Array kopiert. Wenn Sie wissen, dass Sie diese Anzahl überschreiten werden, sollten Sie aus Gründen einer besseren Performance den parametrisierten Konstruktor wählen, der die Übergabe der erforderlichen Startkapazität ermöglicht:
| Dim stack As Stack = New Stack(100) |
Reicht das immer noch nicht aus und wird zur Laufzeit die Initialisierungsgröße trotzdem überschritten, verdoppelt sich die Kapazität automatisch.
Das Beispiel, das vorhin die Klasse Stack veranschaulichte, wird nun auf ein Queue-Objekt umgeschrieben:
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 7\QueueClass |
| ' ---------------------------------------------------------- |
| Module Module1 |
| Sub Main() |
| Dim myQueue As New Queue |
| ' Queue füllen |
| Dim i As Integer |
| For i = 0 To 10 |
| myQueue.Enqueue(i * i) |
| Next |
| ' Ausgabe an der Konsole |
| PrintStack(myQueue) |
| Console.ReadLine() |
| End Sub |
| Public Sub PrintStack(ByVal obj As Queue) |
| ' alle Elemente aus dem Stack holen |
| Do While (obj.Count <> 0) |
| Console.WriteLine(obj.Dequeue()) |
| Loop |
| End Sub |
| End Module |
Diesmal sind es die beiden Methoden Enqueue und Dequeue, mit denen Elemente in die Liste geschoben und wieder aus ihr geholt werden. Dequeue liefert nicht nur die Referenz des sich am Anfang befindlichen Elements, es holt dieses Element auch aus der Warteschlange. Wie bei der Klasse Stack können Sie sich mit Peek auch die Referenz dieses Elements besorgen und es gleichzeitig in der Liste lassen.
Der Elementzugriff erfolgt in derselben Reihenfolge, in der die Objekte der Liste hinzugefügt wurden: Das erste hinzugefügte Element wird auch als erstes herausgeholt, danach kann man das zweite in die Warteschlange gelegte holen usw. Ein Zugriff auf ein beliebiges Element ist weder beim Stack noch bei der Queue möglich.
Die Standardkapazität eines Queue-Objekts beträgt 32 Elemente, die Sie mittels eines anderen Konstruktors bei der Instanziierung bedarfsgerecht festlegen können.
Mit ArrayList, Hashtable, Queue und Stack haben Sie bereits die wichtigsten Auflistungsklassen kennen gelernt. Die .NET-Klassenbibliothek stellt darüber hinaus noch weitere, auf spezifische Anwendungsfälle optimierte Auflistungen bereit, von denen die meisten im Namespace System.Collections.Specialized zu finden sind.
| Hinweis |
|
Genau genommen unterschlage ich Ihnen an dieser Stelle eine ganz neue Gruppe von Auflistungen, die seit dem .NET Framework 2.0 verfügbar sind; denn die Auflistungen unterteilen sich in zwei Gruppen: untypisierte Auflistungen typisierte Auflistungen (generische Auflistungen)Generische Auflistungen wurden mit .NET Framework 2.0 eingeführt und bieten gegenüber den untypisierten den Vorteil, dass sie bereits zur Entwicklungszeit auf einen bestimmten Typ geprägt werden können, d. h., es wird ein ganz bestimmter Typ verwaltet. Generische Auflistungen finden Sie im Namespace System.Collections.Generic. Mit generischen Typen befassen wir uns im nächsten Abschnitt. |
In der folgenden Tabelle erhalten Sie einen Überblick über die Auflistungsklassen, mit denen wir uns nicht näher beschäftigt haben. Da wir uns bereits einige typische Auflistungen genauer angesehen haben, ist es sicherlich nicht mehr schwierig, sich im Bedarfsfall in die Fähigkeiten einer anderen einzuarbeiten. Letztendlich finden wir immer die gleichen Eigenschaften und Methoden vor, die sich meist nur in der Parameterliste unterscheiden.
| Klasse | Beschreibung |
| BitArray | Verwaltet einen Array von Bits. |
| CollectionsUtil | Eine Auflistung, bei der keine Unterscheidung zwischen Groß- und Kleinschreibung erfolgt. |
| HybridDictionary | Das Verhalten orientiert sich an der Anzahl der Listenelemente. Ist die Anzahl der Elemente gering, operiert diese Klasse als ListDictionary-Collection, wird die Anzahl größer, als Hashtable. |
| ListDictionary | Solange die Anzahl der Elemente kleiner 10 ist, werden die Operationen mit den Elementen schneller ausgeführt als bei einer Hashtable. |
| NameValueCollection | Verwaltet ein Schlüssel-Wert-Paar, wobei sowohl der Schlüssel als auch der Wert durch Zeichenfolgen beschrieben werden. Einem Schlüssel können mehrere Zeichenfolgen zugeordnet werden, d. h., der Schlüssel ist nicht eindeutig. |
| SortedList | Diese Auflistung verwaltet Schlüssel-Wert-Paare, die nach den Schlüsseln sortiert sind und auf die sowohl über Schlüssel als auch über Indizes zugegriffen werden kann. Damit vereint sie die Merkmale von Hashtable und ArrayList. |
| StringCollection | Eine Auflistung, die nur Zeichenfolgen enthält. |
| StringDictionary | Ähnlich einer Hashtable, der Schlüssel ist jedoch immer eine Zeichenfolge. |
Im Einzelfall kann es sich als schwierig erweisen, aus der großen Anzahl der angebotenen Typen die für die aktuellen Anforderungen am besten geeignete zu wählen. Im Ausschlussverfahren sollten Sie sich dem Typ nähern, der Ihnen unter den gegebenen Umständen die maximale Performance und das gewünschte Verhalten bietet.
Wissen Sie, dass der wahlfreie, also beliebige Zugriff auf die Listenelemente erforderlich ist, verabschieden sich bereits die ersten beiden Klassen aus dem Angebot (Stack und Queue). Das nächste Kriterium auf dem Weg zur Entscheidungsfindung dürfte die Antwort auf die Frage sein, ob die Verwaltung über einen Index gewünscht oder sogar gefordert wird. Das könnte beispielsweise der Fall sein, wenn in einer Schleife über einen Schleifenzähler die Listenelemente der Reihe nach besucht werden müssen. Die Wahl würde in diesem Fall ArrayList oder SortedList lauten.
Objekte, die sich durch eine Schlüssel-Wert-Kombination beschreiben lassen, werden meist in Auflistungen verwaltet, die nicht indexbasiert sind. Ist die Anzahl der Elemente sehr klein, würde sich der Typ ListDictionary anbieten, ist die Anzahl größer, eignet sich besser Hashtable. Falls Sie keinen Mut zur Entscheidung haben – mit HybridDictionary geben Sie die Verantwortung ab. Liegt eine Schlüssel-Wert-Kombination vor und können Sie dennoch nicht auf die Elementsortierung in der Liste verzichten, lautet die Entscheidung wieder SortedList.
In speziellen Sonderfällen wird man auch noch einen Blick auf andere Typen werfen müssen, aber mit den eben erwähnten sind sicherlich 95 % aller Anwendungsfälle abzudecken.
Sie suchen eine Auflistungsklasse, die ausschließlich bestimmte Typen verwaltet? Möglicherweise sogar Objekte eines benutzerdefinierten Typs? Sie werden mit Sicherheit keine passende Klasse mit der geforderten strikten Typbindung im .NET Framework finden. Sie haben jetzt zwei Alternativen:
| Sie leiten eine passende, vom .NET Framework zur Verfügung gestellte Klasse ab, z. B. CollectionBase. |
| Sie entwickeln eine generische Auflistungsklasse oder benutzen eine aus der .NET-Klassenbibliothek. |
Ich werde Ihnen zuerst zeigen, wie Sie eine eigene Auflistungsklasse durch Ableitung bereitstellen. In Abschnitt 7.3 werden wir uns mit den Generics auseinander setzen, die einen anderen, sicherlich auch einfacheren Weg aufzeigen. Nichtsdestotrotz halte ich es für sehr lehrreich, sich den Weg über die Ableitung anzusehen. Lassen Sie uns deshalb damit jetzt starten.
Angenommen wir hätten eine Klasse namens HoldValue entwickelt und wollen viele Objekte dieses Typs von einer Auflistung verwalten lassen. Ein erster Ansatz könnte sein, eine neue Klasse bereitzustellen, die ein Objekt vom Typ ArrayList aggregiert. Von unserer Auflistungsklasse werden Methoden und Eigenschaften veröffentlicht, die es ermöglichen, durch Weiterleitung die Methoden und Eigenschaften des internen ArrayList-Objekts zu bedienen.
| Class HoldValue |
| Public Value As Integer |
| Public Sub New(ByVal val As Integer) |
| value = value |